
/**
 * EventSource/Server-Sent Events parser
 * @see https://html.spec.whatwg.org/multipage/server-sent-events.html
 */

export function createParser(onParse) {
  let isFirstChunk: boolean
  let buffer: string
  let startingPosition: number
  let startingFieldLength: number
  let eventId: string | undefined
  let eventName: string | undefined
  let data: string

  reset();
  return { feed, reset };
  function reset(): void {
    isFirstChunk = true;
    buffer = '';
    startingPosition = 0;
    startingFieldLength = -1;
    eventId = undefined;
    eventName = undefined;
    data = '';
  };
  function feed(chunk: string): void {
    buffer = buffer ? buffer + chunk : chunk;
    if (isFirstChunk && hasBom(buffer)) {
      buffer = buffer.slice(BOM.length);
    }
    isFirstChunk = false;
    const length = buffer.length;
    let position = 0;
    let discardTrailingNewline = false;
    while (position < length) {
      if (discardTrailingNewline) {
        if (buffer[position] === '\n') {
          ++position;
        }
        discardTrailingNewline = false;
      };
      let lineLength = -1;
      let fieldLength = startingFieldLength;
      let character: string;
      for (let index = startingPosition; lineLength < 0 && index < length; ++index) {
        character = buffer[index];
        if (character === ':' && fieldLength < 0) {
          fieldLength = index - position;
        } else if (character === '\r') {
          discardTrailingNewline = true;
          lineLength = index - position;
        } else if (character === '\n') {
          lineLength = index - position;
        }
      }

      if (lineLength < 0) {
        startingPosition = length - position;
        startingFieldLength = fieldLength;
        break
      } else {
        startingPosition = 0;
        startingFieldLength = -1
      }
      parseEventStreamLine(buffer, position, fieldLength, lineLength);
      position += lineLength + 1;
    }
    if (position === length) {
      buffer = '';
    } else if (position > 0) {
      buffer = buffer.slice(position);
    }
  };
  function parseEventStreamLine(
    lineBuffer: string,
    index: number,
    fieldLength: number,
    lineLength: number,
  ) {
    if (lineLength === 0) {
      if (data.length > 0) {
        onParse({
          type: 'event',
          id: eventId,
          event: eventName || undefined,
          data: data.slice(0, -1),
        });
        data = '';
        eventId = undefined;
      }
      eventName = undefined;
      return;
    }
    const noValue = fieldLength < 0;
    const field = lineBuffer.slice(index, index + (noValue ? lineLength : fieldLength));
    let step = 0;
    if (noValue) {
      step = lineLength;
    } else if (lineBuffer[index + fieldLength + 1] === ' ') {
      step = fieldLength + 2;
    } else {
      step = fieldLength + 1;
    }
    const position = index + step;
    const valueLength = lineLength - step;
    const value = lineBuffer.slice(position, position + valueLength).toString();
    if (field === 'data') {
      data += value ? `${value}\n` : '\n';
    } else if (field === 'event') {
      eventName = value;
    } else if (field === 'id' && !value.includes('\u0000')) {
      eventId = value;
    } else if (field === 'retry') {
      const retry = parseInt(value, 10);
      if (!Number.isNaN(retry)) {
        onParse({ type: 'reconnect-interval', value: retry });
      }
    }
  }
}
const BOM = [239, 187, 191];
function hasBom(buffer: string) {
  return BOM.every((charCode: number, index: number) => buffer.charCodeAt(index) === charCode);
}
